//
//  MujiMediaLoadRequestManager.m
//  ECCoreLibrary
//
//  Created by Paul Cantrell on 6/11/14.
//  Copyright (c) 2014 EnglishCentral, Inc. All rights reserved.
//

#import "MujiMediaLoadRequestManager.h"
#import "MujiMediaLoadRequest.h"
//#import "ECFilePathUtility.h"
//#import "Cocoalumberjack.h"

//#ifdef DEBUG
//static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
//#else
//static const DDLogLevel ddLogLevel = DDLogLevelError;
//#endif

@interface MujiMediaLoadRequestManager ()

@property (nonatomic,strong) NSDictionary *requestDictionary;
@property (readwrite, nonatomic, copy) NSString *moviesDirectoryPath;
@property (nonatomic,strong) NSString *bomDirectoryPath;

@end

@implementation MujiMediaLoadRequestManager

+ (MujiMediaLoadRequestManager *)manager
{
   static MujiMediaLoadRequestManager *gRequestManager = nil;
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^
   {
      gRequestManager = [[MujiMediaLoadRequestManager alloc] init];
   });
    
   return gRequestManager;
}


/**
  * init - Initialize the Media Load Request Manager
  * 
  * We create our cache directory if it doesn't exist, and we inventory all the cache files that
  * exist and coorelate them to the files that we expect. We clean up any we don't recognize (they
  * are probably partial downloads) and then initialize download requests for each of the files
  * that are completed downloads.
 **/

- (id)init
{
   self = [super init];
   if (self)
   {
      NSFileManager *fm         = [NSFileManager defaultManager];
      NSArray *paths            = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
      NSString *baseDir         = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
      _moviesDirectoryPath      = [[baseDir stringByAppendingPathComponent:@"MoviesCache"] copy];
      BOOL isDir                = NO;
      BOOL cacheDirectoryExists = [fm fileExistsAtPath:_moviesDirectoryPath isDirectory:&isDir];
      if( cacheDirectoryExists && !isDir )
      {
         [fm removeItemAtPath:_moviesDirectoryPath error:nil];
         cacheDirectoryExists = NO;
      }
      if(!cacheDirectoryExists)
      {
         NSError *createError;
         [fm createDirectoryAtPath:_moviesDirectoryPath
       withIntermediateDirectories:YES
                        attributes:nil
                             error:&createError];
      }
      
      // if the bill-of-materials dictionary exists on disk read it in
      _bomDirectoryPath  = [_moviesDirectoryPath stringByAppendingPathComponent:@"bom.pinfo"];
       
       // Make sure old version classes are using correct class names.
       [NSKeyedUnarchiver setClass:[MujiMediaLoadRequest class] forClassName:@"MujiMediaLoadRequestV7"];
       [NSKeyedArchiver setClassName:NSStringFromClass([MujiMediaLoadRequest class]) forClass:NSClassFromString(@"MujiMediaLoadRequestV7")];

       
      _requestDictionary = [NSKeyedUnarchiver unarchiveObjectWithFile:_bomDirectoryPath];
      
      // delete any non-completed downloads and inventory the cache files we expect to find
      NSMutableArray *knownLoadFiles = [NSMutableArray arrayWithCapacity:[_requestDictionary count]];
      NSMutableDictionary *mRequests = [NSMutableDictionary dictionaryWithDictionary:_requestDictionary];
      for( MujiMediaLoadRequest *loadRequest in [_requestDictionary allValues] )
      {
         if( !loadRequest.downloadHasFinished || ![loadRequest localFileIsPresent] )
         {
            [loadRequest uncache];
            [mRequests removeObjectForKey:[loadRequest originalUrlString]];
         }
         else
         {
            [knownLoadFiles addObject:[loadRequest localFilename]];
         }
      }
      
      _requestDictionary = [NSDictionary dictionaryWithDictionary:mRequests];
      
      // clean up any files that aren't in our bill of materials
      NSDirectoryEnumerator *dirEnum = [fm enumeratorAtPath:_moviesDirectoryPath];
      NSString *localFile;
      while ((localFile = [dirEnum nextObject]))
      {
         if( ![localFile isEqualToString:@"bom.pinfo"] )
         {
            if( ![knownLoadFiles containsObject:localFile] )
            {
               NSError *error;
               NSString *filename = [_moviesDirectoryPath stringByAppendingPathComponent:localFile];
               [fm removeItemAtPath:filename error:&error];
            }
         }
      }
   }
   
   
   return self;
}

/**
 * If the url has already been downloaded, return the path to the local file
 */
- (NSString *)localFileForUrlString:(NSString *)urlString
{
    MujiMediaLoadRequest *request = [_requestDictionary objectForKey:urlString];
    if([request downloadHasFinished])
    {
        NSString *filename = [request localFilename];
        NSString *filePath = [_moviesDirectoryPath stringByAppendingPathComponent:filename];
        return filePath;
    }
    else
    {
        return nil;
    }
}

// Public method to change the date on a request (typically to make it most recently accessed)

- (void)setDate:(NSDate *)date forRequest:(MujiMediaLoadRequest *)request
{
    [request setDate:[NSDate date]];    // Update the time to reflect recent access to request
    [self saveBOM];
}

- (void)saveBOM
{
    [NSKeyedArchiver archiveRootObject:_requestDictionary toFile:_bomDirectoryPath];
}

// Public method to see if a url has been cached and is available as a local movie file

- (MujiMediaLoadRequest *)requestWithUrlString:(NSString *)urlString
{
    MujiMediaLoadRequest *request = [_requestDictionary objectForKey:urlString];
    if(!request)
    {
        request = [[MujiMediaLoadRequest alloc] initWithUrlString:urlString];
        [request addAsDelegate:self];
        NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:_requestDictionary];
        [mDict setObject:request forKey:urlString];
        _requestDictionary = [NSDictionary dictionaryWithDictionary:mDict];
    }
   
    return request;
}

// Public method to find a request based on the local filename which is itself based on a GUID
//  and therefore known to be a unique string.

- (MujiMediaLoadRequest *)existingRequestWithLocalFilename:(NSString *)localFilename
{
    for(MujiMediaLoadRequest *request in [_requestDictionary allValues])
    {
        if([request.localFilename isEqualToString:localFilename])
        {
            return request;
        }
    }
   
    return nil;
}

// Public Method to stop any downloads in progress. It makes sense to call this when the user
// selects a new movie to play. It would probably be bad to try to download two movies at once,
// and generally the download progresses faster than the movie plays, so if the user watches
// most of the movie, the download will probably have finished and is safely cached. If the user
// watches a few seconds of a movie and then switches (because it wasn't the movie they wanted)
// they probably won't want to watch it again so it probably wouldn't make sense to try to complete
// that download. Note that this method won't affect movies that completed download, so it won't
// uncache a completely downloaded movie.

- (void)cancelDownloadsInProgress
{
   NSMutableDictionary *mRequestDictionary = [_requestDictionary mutableCopy];
    for(MujiMediaLoadRequest *request in [_requestDictionary allValues])
    {
        if([request cancelDownload])
        {
            [request uncache];
            [mRequestDictionary removeObjectForKey:request.originalUrlString];
        }
    }
   
    _requestDictionary = [NSDictionary dictionaryWithDictionary:mRequestDictionary];
}

// Public Method to walk all the requests and remove older ones which would cause the storage
// to exceed the specified size.

- (void)trimDataUsageTo:(UInt64)bytes
{
    NSSortDescriptor *sortDesc              = [NSSortDescriptor sortDescriptorWithKey:@"createDate" ascending:NO];
    NSArray *sortedByDate                   = [[_requestDictionary allValues] sortedArrayUsingDescriptors:@[sortDesc]];
    NSMutableDictionary *mRequestDictionary = [NSMutableDictionary dictionaryWithDictionary:_requestDictionary];
    UInt64 runningTotal                     = 0;
    for(MujiMediaLoadRequest *request in sortedByDate)
    {
        if(runningTotal + request.contentLength > bytes)
        {
            [request cancelDownload];
            [request uncache];
            [mRequestDictionary removeObjectForKey:request.originalUrlString];
        }
        else
        {
            runningTotal += request.contentLength;
        }
    }
    
   // DDLogInfo(@"%@ :: Trimmed running total of bytes:%lld", NSStringFromClass([self class]), runningTotal);
    _requestDictionary = [NSDictionary dictionaryWithDictionary:mRequestDictionary];
}

// Public method to return how much space is in use

- (UInt64)cachedSpaceInUse
{
    UInt64 runningTotal = 0;
    for(MujiMediaLoadRequest *request in [_requestDictionary allValues])
    {
        runningTotal += request.contentLength;
    }
    return runningTotal;
}

#pragma mark - Media Load Request Delegate Callbacks

/**
 * We only care about getting the download completed callback, but these are all non-optional so
 * we just implement no-ops for the other delegate methods
 */

- (void)newDataHasArrived
{
}

- (void)downloadHasBegun
{
}

- (void)downloadHasCompleted;
{
    [self saveBOM];
}

- (void)downloadFailed:(MujiMediaLoadRequest *)mediaLoadRequest withError:(NSError *)error
{
   if (mediaLoadRequest)
   {
      NSMutableDictionary *mRequestDictionary = [_requestDictionary mutableCopy];
      
      @try
      {
         [mediaLoadRequest cancelDownload];
         [mediaLoadRequest uncache];
         [mRequestDictionary removeObjectForKey:mediaLoadRequest.originalUrlString];
      }
      @catch (NSException *__unused exception) {}
      @finally
      {
         _requestDictionary = [NSDictionary dictionaryWithDictionary:mRequestDictionary];
         [self saveBOM];
      }
   }
}

#pragma mark - 
#pragma mark MujiApplicationStateManagementProtocol

- (void)manageApplicationStateTransition:(MujiApplicationStateTransition)stateTransition
{
    switch (stateTransition)
    {
        case MujiApplicationStateTransitionDidBecomeActive:
            break;
        case MujiApplicationStateTransitionDidEnterBackground:
            [self cancelDownloadsInProgress];
            [self trimDataUsageTo:(5 * 1024 * 1024)];
            break;
        case MujiApplicationStateTransitionDidFinishLaunching:
            break;
        case MujiApplicationStateTransitionWillEnterForeground:
            break;
        case MujiApplicationStateTransitionWillResignActive:
            break;
        case MujiApplicationStateTransitionWillTerminate:
            [self cancelDownloadsInProgress];
            [self trimDataUsageTo:(5 * 1024 * 1024)];
            break;
        default:
            break;
    }
}

- (void)manageApplicationStateTransition:(MujiApplicationStateTransition)stateTransition
                withApplicationLifecycle:(MujiApplicationLifecycle *)applicationLifecycle
{
    switch (stateTransition)
    {
        case MujiApplicationStateTransitionDidBecomeActive:
            break;
        case MujiApplicationStateTransitionDidEnterBackground:
            [self cancelDownloadsInProgress];
            [self trimDataUsageTo:(5 * 1024 * 1024)];
            break;
        case MujiApplicationStateTransitionDidFinishLaunching:
            break;
        case MujiApplicationStateTransitionWillEnterForeground:
            break;
        case MujiApplicationStateTransitionWillResignActive:
            break;
        case MujiApplicationStateTransitionWillTerminate:
            [self cancelDownloadsInProgress];
            [self trimDataUsageTo:(5 * 1024 * 1024)];
            break;
        default:
            break;
    }
}


@end
